/*******************************************************************************
 * Copyright (c) 2003, 2009 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.progress;

import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.*;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.osgi.util.NLS;
import org.eclipse.rap.ui.internal.progress.ProgressUtil;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.internal.progress.ProgressMessages;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.model.IWorkbenchAdapter;

/**
 * The DeferredContentManager is a class that helps an ITreeContentProvider get
 * its deferred input.
 * 
 * <b>NOTE</b> AbstractTreeViewer#isExpandable may need to be implemented in
 * AbstractTreeViewer subclasses with deferred content that use filtering as a
 * call to #getChildren may be required to determine the correct state of the
 * expanding control.
 * 
 * AbstractTreeViewers which use this class may wish to sacrifice accuracy of
 * the expandable state indicator for the performance benefits of deferring
 * content.
 * 
 * @see IDeferredWorkbenchAdapter
 * @since 1.0
 */
public class DeferredTreeContentManager {

	AbstractTreeViewer treeViewer;

	IWorkbenchSiteProgressService progressService;

	private ListenerList updateCompleteListenerList;

    // RAP [if] display used to access NLS messages   
    private final Display display;

	/**
	 * The DeferredContentFamily is a class used to keep track of a
	 * manager-object pair so that only jobs scheduled by the receiver are
	 * canceled by the receiver.
	 */
	class DeferredContentFamily {
		protected DeferredTreeContentManager manager;
		protected Object element;

		/**
		 * Create a new instance of the receiver to define a family for object
		 * in a particular scheduling manager.
		 * 
		 * @param schedulingManager
		 * @param object
		 */
		DeferredContentFamily(DeferredTreeContentManager schedulingManager,
				Object object) {
			this.manager = schedulingManager;
			this.element = object;
		}
	}

	/**
	 * Create a new instance of the receiver using the supplied content provider
	 * and viewer. Run any jobs using the site.
	 * 
	 * @param provider
	 * @param viewer
	 * @param site
	 * @deprecated in 3.4. provider is not used by this class
	 */
	public DeferredTreeContentManager(ITreeContentProvider provider,
			AbstractTreeViewer viewer, IWorkbenchPartSite site) {
		this(viewer, site);
	}

	/**
	 * Create a new instance of the receiver using the supplied content provider
	 * and viewer.
	 * 
	 * @param provider
	 *            The content provider that will be updated
	 * @param viewer
	 *            The tree viewer that the results are added to
	 * @deprecated in 3.4. provider is not used by this class
	 */
	public DeferredTreeContentManager(ITreeContentProvider provider,
			AbstractTreeViewer viewer) {
		this(viewer);
	}

	/**
	 * Create a new instance of the receiver using the supplied content provider
	 * and viewer. Run any jobs using the site.
	 * 
	 * @param viewer
	 * @param site
	 * 
	 * @since 1.1
	 */
	public DeferredTreeContentManager(AbstractTreeViewer viewer,
			IWorkbenchPartSite site) {
		this(viewer);
		Object siteService = Util.getAdapter(site,
				IWorkbenchSiteProgressService.class);
		if (siteService != null) {
			progressService = (IWorkbenchSiteProgressService) siteService;
		}
	}

	/**
	 * Create a new instance of the receiver using the supplied content provider
	 * and viewer.
	 * 
	 * @param viewer
	 *            The tree viewer that the results are added to
	 * 
	 * @since 1.1
	 */
	public DeferredTreeContentManager(AbstractTreeViewer viewer) {
		treeViewer = viewer;
		// RAP [if] display used to access NLS messages 
		display = treeViewer.getControl().getDisplay();
	}

	/**
	 * Provides an optimized lookup for determining if an element has children.
	 * This is required because elements that are populated lazilly can't answer
	 * <code>getChildren</code> just to determine the potential for children.
	 * Throw an AssertionFailedException if element is null.
	 * 
	 * @param element
	 *            The Object being tested. This should not be <code>null</code>.
	 * @return boolean <code>true</code> if there are potentially children.
	 * @throws RuntimeException
	 *             if the element is null.
	 */
	public boolean mayHaveChildren(Object element) {
		Assert.isNotNull(element,
				ProgressMessages.get( display ).DeferredTreeContentManager_NotDeferred);
		IDeferredWorkbenchAdapter adapter = getAdapter(element);
		return adapter != null && adapter.isContainer();
	}

	/**
	 * Returns the child elements of the given element, or in the case of a
	 * deferred element, returns a placeholder. If a deferred element is used, a
	 * job is created to fetch the children in the background.
	 * 
	 * @param parent
	 *            The parent object.
	 * @return Object[] or <code>null</code> if parent is not an instance of
	 *         IDeferredWorkbenchAdapter.
	 */
	public Object[] getChildren(final Object parent) {
		IDeferredWorkbenchAdapter element = getAdapter(parent);
		if (element == null) {
			return null;
		}
		PendingUpdateAdapter placeholder = createPendingUpdateAdapter();
		startFetchingDeferredChildren(parent, element, placeholder);
		return new Object[] { placeholder };
	}

	/**
	 * Factory method for creating the pending update adapter representing the
	 * placeholder node. Subclasses may override.
	 * 
	 * @return a pending update adapter
	 * @since 1.0
	 */
	protected PendingUpdateAdapter createPendingUpdateAdapter() {
		return new PendingUpdateAdapter();
	}

	/**
	 * Return the IDeferredWorkbenchAdapter for element or the element if it is
	 * an instance of IDeferredWorkbenchAdapter. If it does not exist return
	 * null.
	 * 
	 * @param element
	 * @return IDeferredWorkbenchAdapter or <code>null</code>
	 */
	protected IDeferredWorkbenchAdapter getAdapter(Object element) {
		return (IDeferredWorkbenchAdapter) Util.getAdapter(element,
				IDeferredWorkbenchAdapter.class);
	}

	/**
	 * Starts a job and creates a collector for fetching the children of this
	 * deferred adapter. If children are waiting to be retrieved for this parent
	 * already, that job is cancelled and another is started.
	 * 
	 * @param parent
	 *            The parent object being filled in,
	 * @param adapter
	 *            The adapter being used to fetch the children.
	 * @param placeholder
	 *            The adapter that will be used to indicate that results are
	 *            pending.
	 */
	protected void startFetchingDeferredChildren(final Object parent,
			final IDeferredWorkbenchAdapter adapter,
			final PendingUpdateAdapter placeholder) {
		final IElementCollector collector = createElementCollector(parent,
				placeholder);
		// Cancel any jobs currently fetching children for the same parent
		// instance.
		cancel(parent);
		String jobName = getFetchJobName(parent, adapter);
		Job job = new Job(jobName) {
			/*
			 * (non-Javadoc)
			 * 
			 * @see org.eclipse.core.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
			 */
			public IStatus run(IProgressMonitor monitor) {
				adapter.fetchDeferredChildren(parent, collector, monitor);
				if (monitor.isCanceled()) {
					return Status.CANCEL_STATUS;
				}
				return Status.OK_STATUS;
			}

			/*
			 * (non-Javadoc)
			 * 
			 * @see org.eclipse.core.jobs.Job#belongsTo(java.lang.Object)
			 */
			public boolean belongsTo(Object family) {
				if (family instanceof DeferredContentFamily) {
					DeferredContentFamily contentFamily = (DeferredContentFamily) family;
					if (contentFamily.manager == DeferredTreeContentManager.this) {
						return isParent(contentFamily, parent);
					}
				}
				return false;

			}

			/**
			 * Check if the parent of element is equal to the parent used in
			 * this job.
			 * 
			 * @param family
			 *            The DeferredContentFamily that defines a potential
			 *            ancestor of the current parent in a particular
			 *            manager.
			 * @param child
			 *            The object to check against.
			 * @return boolean <code>true</code> if the child or one of its
			 *         parents are the same as the element of the family.
			 */
			private boolean isParent(DeferredContentFamily family, Object child) {
				if (family.element.equals(child)) {
					return true;
				}
				IWorkbenchAdapter workbenchAdapter = getWorkbenchAdapter(child);
				if (workbenchAdapter == null) {
					return false;
				}
				Object elementParent = workbenchAdapter.getParent(child);
				if (elementParent == null) {
					return false;
				}
				return isParent(family, elementParent);
			}

			/**
			 * Get the workbench adapter for the element.
			 * 
			 * @param element
			 *            The object we are adapting to.
			 */
			private IWorkbenchAdapter getWorkbenchAdapter(Object element) {
				return (IWorkbenchAdapter) Util.getAdapter(element,
						IWorkbenchAdapter.class);
			}
		};
		job.addJobChangeListener(new JobChangeAdapter() {
			/*
			 * (non-Javadoc)
			 * 
			 * @see org.eclipse.core.runtime.jobs.JobChangeAdapter#done(org.eclipse.core.runtime.jobs.IJobChangeEvent)
			 */
			public void done(IJobChangeEvent event) {
				runClearPlaceholderJob(placeholder);
			}
		});
		job.setRule(adapter.getRule(parent));
		if (progressService == null) {
			job.schedule();
		} else {
			progressService.schedule(job);
		}
	}

	/**
	 * Returns a name to use for the job that fetches children of the given
	 * parent. Subclasses may override. Default job name is parent's label.
	 * 
	 * @param parent
	 *            parent that children are to be fetched for
	 * @param adapter
	 *            parent's deferred adapter
	 * @return job name
	 */
	protected String getFetchJobName(Object parent,
			IDeferredWorkbenchAdapter adapter) {
		return NLS.bind(
				ProgressMessages.get( display ).DeferredTreeContentManager_FetchingName,
				adapter.getLabel(parent));
	}

	/**
	 * Create a UIJob to add the children to the parent in the tree viewer.
	 * 
	 * @param parent
	 * @param children
	 * @param monitor
	 */
	protected void addChildren(final Object parent, final Object[] children,
			IProgressMonitor monitor) {
		WorkbenchJob updateJob = new WorkbenchJob( display,
				ProgressMessages.get( display ).DeferredTreeContentManager_AddingChildren) {
			/*
			 * (non-Javadoc)
			 * 
			 * @see org.eclipse.ui.progress.UIJob#runInUIThread(org.eclipse.core.runtime.IProgressMonitor)
			 */
			public IStatus runInUIThread(IProgressMonitor updateMonitor) {
				// Cancel the job if the tree viewer got closed
				if (treeViewer.getControl().isDisposed()
						|| updateMonitor.isCanceled()) {
					return Status.CANCEL_STATUS;
				}
				treeViewer.add(parent, children);
				return Status.OK_STATUS;
			}
		};
		updateJob.setSystem(true);
		updateJob.schedule();

	}

	/**
	 * Return whether or not the element is or adapts to an
	 * IDeferredWorkbenchAdapter.
	 * 
	 * @param element
	 * @return boolean <code>true</code> if the element is an
	 *         IDeferredWorkbenchAdapter
	 */
	public boolean isDeferredAdapter(Object element) {
		return getAdapter(element) != null;
	}

	/**
	 * Run a job to clear the placeholder. This is used when the update for the
	 * tree is complete so that the user is aware that no more updates are
	 * pending.
	 * 
	 * @param placeholder
	 */
	protected void runClearPlaceholderJob(final PendingUpdateAdapter placeholder) {
// RAP [fappel]: uses session aware approach
//		if (placeholder.isRemoved() || !PlatformUI.isWorkbenchRunning()) {
//			return;
//		}
        if (placeholder.isRemoved() || !ProgressUtil.isWorkbenchRunning( display )) {
			return;
		}

		// Clear the placeholder if it is still there
		WorkbenchJob clearJob = new WorkbenchJob( display,
				ProgressMessages.get( display ).DeferredTreeContentManager_ClearJob) {
			/*
			 * (non-Javadoc)
			 * 
			 * @see org.eclipse.ui.progress.UIJob#runInUIThread(org.eclipse.core.runtime.IProgressMonitor)
			 */
			public IStatus runInUIThread(IProgressMonitor monitor) {
				if (!placeholder.isRemoved()) {
					Control control = treeViewer.getControl();
					if (control.isDisposed()) {
						return Status.CANCEL_STATUS;
					}
					treeViewer.remove(placeholder);
					placeholder.setRemoved(true);
				}
				return Status.OK_STATUS;
			}
		};
		clearJob.setSystem(true);
		
		if (updateCompleteListenerList != null) {
			Object[] listeners = updateCompleteListenerList.getListeners();
			for (int i = 0; i < listeners.length; i++) {
				clearJob
						.addJobChangeListener((IJobChangeListener) listeners[i]);
			}
		}
		clearJob.schedule();
	}

	/**
	 * Cancel all jobs that are fetching content for the given parent or any of
	 * its children.
	 * 
	 * @param parent
	 */
	public void cancel(Object parent) {
		if (parent == null) {
			return;
		}

		Job.getJobManager().cancel(new DeferredContentFamily(this, parent));
	}

	/**
	 * Create the element collector for the receiver.
	 * 
	 * @param parent
	 *            The parent object being filled in,
	 * @param placeholder
	 *            The adapter that will be used to indicate that results are
	 *            pending.
	 * @return IElementCollector
	 */
	protected IElementCollector createElementCollector(final Object parent,
			final PendingUpdateAdapter placeholder) {
		return new IElementCollector() {
			/*
			 * (non-Javadoc)
			 * 
			 * @see org.eclipse.jface.progress.IElementCollector#add(java.lang.Object,
			 *      org.eclipse.core.runtime.IProgressMonitor)
			 */
			public void add(Object element, IProgressMonitor monitor) {
				add(new Object[] { element }, monitor);
			}

			/*
			 * (non-Javadoc)
			 * 
			 * @see org.eclipse.jface.progress.IElementCollector#add(java.lang.Object[],
			 *      org.eclipse.core.runtime.IProgressMonitor)
			 */
			public void add(Object[] elements, IProgressMonitor monitor) {
				addChildren(parent, elements, monitor);
			}

			/*
			 * (non-Javadoc)
			 * 
			 * @see org.eclipse.jface.progress.IElementCollector#done()
			 */
			public void done() {
				runClearPlaceholderJob(placeholder);
			}
		};
	}

	/**
	 * Add a listener to list of update complete listeners. These listeners are
	 * attached to the job that updates the viewer content (clears the pending
	 * entry, etc.) after all deferred content has been retrieved.
	 * 
	 * This method has no effect if the listener has already been added to the
	 * list of listeners.
	 * 
	 * Since 3.6, this listener is added to a list of listeners rather than
	 * replacing the previously added listener. For backward compatibility,
	 * adding a null listener will be interpreted as removal of a listener if
	 * only one listener has been registered.
	 * 
	 * @param listener
	 *            the listener to add to the list of update listeners
	 * @since 1.1
	 */
	public void addUpdateCompleteListener(IJobChangeListener listener){
		// Maintain backward compatibility.
		// Earlier only one listener was supported, so it can be removed by
		// passing null
		if (listener == null && updateCompleteListenerList != null) {
			Object[] listeners = updateCompleteListenerList.getListeners();
			if (listeners.length == 1) {
				removeUpdateCompleteListener((IJobChangeListener) listeners[0]);
			}
		} else {
			if (updateCompleteListenerList == null) {
				updateCompleteListenerList = new ListenerList();
			}
			updateCompleteListenerList.add(listener);
		}
	}

	/**
	 * Removes the listener from the list of update listeners that are attached
	 * to the job that updates the viewer content (clears the pending entry,
	 * etc.) after all deferred content has been retrieved. If the listener is
	 * already attached to a running job, it is not removed, but it will not be
	 * added to any subsequent jobs that are run.
	 * 
	 * This method has no effect if the listener was not previously added to the
	 * listener list.
	 * 
	 * @param listener
	 *            the listener to be removed
	 * @since 3.6
	 */
	public void removeUpdateCompleteListener(IJobChangeListener listener) {
		if (updateCompleteListenerList != null) {
			updateCompleteListenerList.remove(listener);
		}
	}

}
